package org.objectweb.asm.optimizer;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * A Jar file optimizer.
 * 
 * @author Eric Bruneton
 */
public class JarOptimizer {

  static final Set<String> API = new HashSet<String>();
  static final Map<String, String> HIERARCHY = new HashMap<String, String>();
  static boolean nodebug = false;

  public static void main(final String[] args) throws IOException {
    File f = new File(args[0]);
    InputStream is = new GZIPInputStream(new FileInputStream(f));
    BufferedReader lnr = new LineNumberReader(new InputStreamReader(is));
    while (true) {
      String line = lnr.readLine();
      if (line != null) {
        if (line.startsWith("class")) {
          String c = line.substring(6, line.lastIndexOf(' '));
          String sc = line.substring(line.lastIndexOf(' ') + 1);
          HIERARCHY.put(c, sc);
        } else {
          API.add(line);
        }
      } else {
        break;
      }
    }
    lnr.close();
    int argIndex = 1;
    if (args[argIndex].equals("-nodebug")) {
      nodebug = true;
      argIndex++;
    }

    optimize(new File(args[argIndex]));
  }

  public static void optimize(final File f) throws IOException {
    if (nodebug && f.getName().contains("debug")) {
      return;
    }

    if (f.isDirectory()) {
      File[] files = f.listFiles();
      for (int i = 0; i < files.length; ++i) {
        optimize(files[i]);
      }
    } else if (f.getName().endsWith(".jar")) {
      File g = new File(f.getParentFile(), f.getName() + ".new");
      ZipFile zf = new ZipFile(f);
      ZipOutputStream out = new ZipOutputStream(new FileOutputStream(g));
      Enumeration<? extends ZipEntry> e = zf.entries();
      byte[] buf = new byte[10000];
      while (e.hasMoreElements()) {
        ZipEntry ze = e.nextElement();
        if (ze.isDirectory()) {
          out.putNextEntry(ze);
          continue;
        }
        out.putNextEntry(ze);
        if (ze.getName().endsWith(".class")) {
          ClassReader cr = new ClassReader(zf.getInputStream(ze));
          // cr.accept(new ClassDump(), 0);
          cr.accept(new ClassVerifier(), 0);
        }
        InputStream is = zf.getInputStream(ze);
        int n;
        do {
          n = is.read(buf, 0, buf.length);
          if (n != -1) {
            out.write(buf, 0, n);
          }
        } while (n != -1);
        out.closeEntry();
      }
      out.close();
      zf.close();
      if (!f.delete()) {
        throw new IOException("Cannot delete file " + f);
      }
      if (!g.renameTo(f)) {
        throw new IOException("Cannot rename file " + g);
      }
    }
  }

  static class ClassDump extends ClassVisitor {

    String owner;

    public ClassDump() {
      super(Opcodes.ASM5);
    }

    @Override
    public void visit(final int version, final int access, final String name, final String signature, final String superName,
        final String[] interfaces) {
      owner = name;
      if (owner.startsWith("java/")) {
        System.out.println("class " + name + ' ' + superName);
      }
    }

    @Override
    public FieldVisitor visitField(final int access, final String name, final String desc, final String signature, final Object value) {
      if (owner.startsWith("java/")) {
        System.out.println(owner + ' ' + name);
      }
      return null;
    }

    @Override
    public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
      if (owner.startsWith("java/")) {
        System.out.println(owner + ' ' + name + desc);
      }
      return null;
    }
  }

  static class ClassVerifier extends ClassVisitor {

    String owner;

    String method;

    public ClassVerifier() {
      super(Opcodes.ASM5);
    }

    @Override
    public void visit(final int version, final int access, final String name, final String signature, final String superName,
        final String[] interfaces) {
      owner = name;
    }

    @Override
    public MethodVisitor visitMethod(final int access, final String name, final String desc, final String signature, final String[] exceptions) {
      method = name + desc;
      return new MethodVisitor(Opcodes.ASM5) {
        @Override
        public void visitFieldInsn(final int opcode, final String owner, final String name, final String desc) {
          check(owner, name);
        }

        @Override
        public void visitMethodInsn(final int opcode, final String owner, final String name, final String desc, final boolean itf) {
          check(owner, name + desc);
        }
      };
    }

    void check(String owner, String member) {
      if (owner.startsWith("java/")) {
        String o = owner;
        while (o != null) {
          if (API.contains(o + ' ' + member)) {
            return;
          }
          o = HIERARCHY.get(o);
        }
      }
    }
  }
}
